package com.atguigu.gmall.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.IpUtil;
import jdk.nashorn.internal.runtime.regexp.JoniRegExp;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.print.attribute.standard.PrinterURI;
import java.util.List;

/**
 * @author: atguigu
 * @create: 2023-01-10 14:33
 */
@Component
public class AuthFitler implements GlobalFilter, Ordered {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 需要校验请求地址，要求用户是登录状态才可以访问
     */
    @Value("${authUrls.url}")
    private List<String> authUrlList;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 网关过滤器逻辑
     * a.	限制用户通过浏览器访问内部数据接口
     * <p>
     * b.	限制用户在未登录的情况下访问带有/auth/ 这样的路径
     * <p>
     * c.	限制用户在未登录的情况下访问需要登录的微服务
     * <p>
     * d.	将用户Id，统一存储到header中！
     *
     * @param exchange 封装请求，响应对象
     * @param chain    过滤器链
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();


        //1.限制所有的用户做访问内部微服务间提供带"/inner"接口 直接拒绝
        //1.1 获取用户请求url地址
        String path = request.getURI().getPath();


        //对于样式；JS；图片文件请求直接放行即可
        if (antPathMatcher.match("/**/css/**", path) || antPathMatcher.match("/**/js/**", path) || antPathMatcher.match("/**/img/**", path)) {
            return chain.filter(exchange);
        }


        //1.2 通过路径匹配器进行判断
        if (antPathMatcher.match("/**/inner/**", path)) {
            return outError(response, ResultCodeEnum.PERMISSION);
        }

        //2.尝试从Redis中获取用户ID
        String userId = getUserId(request);
        if (StringUtils.isNotBlank(userId) && userId.equals("-1")) {
            return outError(response, ResultCodeEnum.ILLEGAL_REQUEST);
        }

        //3.限制用户在未登录情况下 访问 /auth 或者 "/api" 接口地址 返回拒绝
        if (antPathMatcher.match("/**/auth/**", path)) {
            if (StringUtils.isBlank(userId)) {
                return outError(response, ResultCodeEnum.LOGIN_AUTH);
            }
        }

        //4.从yml文件读取需要登录后才能访问接口以及地址 判断用户登录状态 重定向到登录页面
        //4.1 遍历需要校验登录状态访问地址列表
        if (!CollectionUtils.isEmpty(authUrlList)) {
            for (String s : authUrlList) {
                if (antPathMatcher.match("/" + s + "*", path) && StringUtils.isBlank(userId)) {
                    //说明用户未登录 访问地址要求登录 设置重定向 需要将http状态码设置为301
                    response.setStatusCode(HttpStatus.SEE_OTHER);
                    //通过Response对象 响应头设置重定向登录地址
                    response.getHeaders().set(HttpHeaders.LOCATION, "http://www.gmall.com/login.html?originUrl=" + request.getURI().toString());
                    //结束
                    return response.setComplete();
                }
            }
        }

        //5.将获取到用户ID设置到请求头中，将用户ID传输到目标微服务
        if (StringUtils.isNotBlank(userId)) {
            request.mutate().header("userId", userId);
        }
        //将获取到临时用户ID设置到请求头
        String userTempId = getUserTempId(request);
        if (StringUtils.isNotBlank(userTempId)) {
            request.mutate().header("userTempId", userTempId);
        }

        return chain.filter(exchange);
    }


    /**
     * 尝试获取临时用户ID
     *
     * @param request
     * @return
     */
    private String getUserTempId(ServerHttpRequest request) {
        String userTempId = "";

        //1.尝试从cookie中获取
        List<HttpCookie> cookieList = request.getCookies().get("userTempId");
        if (!CollectionUtils.isEmpty(cookieList)) {
            userTempId = cookieList.get(0).getValue();
            return userTempId;
        }

        //2.尝试从请求头中获取
        userTempId = request.getHeaders().getFirst("userTempId");
        if(StringUtils.isNotBlank(userTempId)){
            return userTempId;
        }
        return userTempId;
    }


    /**
     * 获取登录用户ID
     * 前端如果访问是静态html文件，令牌采用cookie中提交
     * 前端如果访问是ajax请求，令牌采用请求头中提交
     *
     * @param request
     * @return 正常：用户ID  令牌被窃取：-1
     */
    private String getUserId(ServerHttpRequest request) {
        //1.从请求对象中获取前端提交头信息或者Cookie获取令牌
        String token = "";
        token = request.getHeaders().getFirst("token");
        if (StringUtils.isBlank(token)) {
            List<HttpCookie> cookieList = request.getCookies().get("token");
            if (!CollectionUtils.isEmpty(cookieList)) {
                token = cookieList.get(0).getValue();
            }
        }
        //2.查询Redis中令牌绑定的用户信息  用户信息存在同时 IP 地址也需要相同
        String redisKey = "user:login:" + token;
        JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(redisKey);
        if (jsonObject != null) {
            String ip = jsonObject.getString("ip");
            String userIp = IpUtil.getGatwayIpAddress(request);
            if (userIp.equals(ip)) {
                return jsonObject.getString("userId");
            } else {
                //token被盗取风险
                return "-1";
            }
        }
        return "";
    }


    /**
     * 响应给客户端错误结果
     *
     * @param response   设置响应的结果对象
     * @param permission 封装提示信息
     * @return
     */
    private Mono<Void> outError(ServerHttpResponse response, ResultCodeEnum permission) {
        //封装响应的结果对象
        Result<Object> result = Result.fail().message(permission.getMessage());
        String resultJSON = JSON.toJSONString(result);
        //通过response设置响应数据格式
        response.getHeaders().add("Content-type", "application/json;charset=utf-8");
        DataBuffer wrap = response.bufferFactory().wrap(resultJSON.getBytes());
        return response.writeWith(Mono.just(wrap));
    }


    /**
     * 过滤器执行顺序
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }


    //public static void main(String[] args) {
    //    System.out.println("业务代码1执行");
    //    Mono<String> mono = biz();
    //    System.out.println("业务代码3执行");
    //
    //    //订阅上面任务执行结果
    //    mono.subscribe((ret)->{
    //        System.out.println(ret);
    //    });
    //}
    //
    //
    ///**
    // *
    // * @return
    // */
    //private static Mono<String> biz() {
    //    //创建Mono异步操作-该业务有计算结果-将消息发送到"队列"
    //    Mono<String> mono = Mono.fromCallable(() -> {
    //        Thread.sleep(2000);
    //        System.out.println("业务2代码执行");
    //        return "atguigu-业务2代码";
    //    });
    //    return mono;
    //}


    public static void main(String[] args) {
        //Spring提供地址通配符匹配对象
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/list.html*", "/list.html?category3Id=61");
        System.out.println(match);

    }
}
